/**
 * Copyright (c) 2010 Simple Graph Tools contributors. All rights reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * $Id$
 */
package org.sgt.coredef.util;

import java.util.HashSet;
import java.util.Set;

import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.sgt.coredef.Chain;
import org.sgt.coredef.CoredefFactory;
import org.sgt.coredef.DecomposableNode;
import org.sgt.coredef.DescribedObject;
import org.sgt.coredef.Edge;
import org.sgt.coredef.Node;
import org.sgt.coredef.ProxyEdge;
import org.sgt.coredef.ProxyNode;

/**
 * An helper class for {@link Chain}s.
 * 
 * @author Anne BUTTIGHOFFER
 * 
 */
public final class ChainHelper {

	/**
	 * Helper Class has a private constructor.
	 */
	private ChainHelper() {
		super();
	}

	/**
	 * Determines whether the given Chain is looping i.e.: at least one Node
	 * appears twice in the Node-Edge-Node series.<br/>
	 * <em>For now this implementation will always return false</em>
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @return true only if chain is looping at least on one Node.
	 */
	public static boolean isLooping(final Chain chain) {
		return false;
	}

	/**
	 * Determines whether the given Chain is complete i.e.:
	 * <OL>
	 * <LI>Start Node has an out transition in the chain</LI>
	 * <LI>End Node has an input transition in the chain</LI>
	 * <LI>Each Node (except StartNode and EndNode) has a transition in the
	 * chain as input and output</LI>
	 * <LI>Each Transition has a source and destination in the chain.</LI>
	 * </OL>
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @return true only if chain is complete as defined above.
	 */
	public static boolean isComplete(final Chain chain) {
		boolean complete = false;
		if (chain != null && chain.getStartNode() != null
				&& chain.getEndNode() != null && chain.getEdges() != null
				&& chain.getNodes() != null
				&& chain.getEdges().size() == chain.getNodes().size() + 1) {
			// We might be complete...
			Node startNode = chain.getStartNode();
			Node endNode = chain.getEndNode();
			int edgeSize = chain.getEdges().size();
			Edge firstEdge = chain.getEdges().get(0);
			Edge lastEdge = chain.getEdges().get(edgeSize - 1);

			if (startNode != firstEdge.getSource()
					&& firstEdge.getDestination() != chain.getNodes().get(0)) {
				return false;
			}

			if (chain.getNodes().get(edgeSize - 2) != lastEdge.getSource()
					|| endNode != lastEdge.getDestination()) {
				return false;
			}

			for (int i = 1; i < chain.getNodes().size(); i++) {
				Edge edge = chain.getEdges().get(i);
				Node source = chain.getNodes().get(i - 1);
				Node destination = chain.getNodes().get(i);
				if (edge != null && source != edge.getSource()
						&& destination != edge.getDestination()) {
					return false;
				}
			}
			complete = true;
		}
		return complete;
	}

	/**
	 * Returns the Node following the given Node in given Chain. i.e. the one
	 * having a transition which has the given Node as source and returned Node
	 * as destination. Null is to be returned in case :
	 * <UL>
	 * <LI>node is null</LI>
	 * <LI>no solution or multiple solutions are found</LI>
	 * </UL>
	 * .
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @param node
	 *            {@link Node}
	 * @return the next node in the chain (may be null).
	 */
	public static Node nextNode(final Chain chain, final Node node) {
		if (node == null) {
			return null;
		}
		Set<Edge> possibleEdges = new HashSet<Edge>();
		for (Edge edge : chain.getEdges()) {
			if (edge.getSource() == node) {
				possibleEdges.add(edge);
			}
		}
		if (possibleEdges.size() == 1) {
			return possibleEdges.iterator().next().getDestination();
		}
		return null;
	}

	/**
	 * Returns the Node preceding the given Node in given Chain. i.e. the one
	 * having a transition which has the given Node as destination and returned
	 * Node as source. Null is to be returned in case :
	 * <UL>
	 * <LI>node is null</LI>
	 * <LI>no solution or multiple solutions are found</LI>
	 * </UL>
	 * .
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @param node
	 *            {@link Node}
	 * @return the next node in the chain (may be null).
	 */
	public static Node previousNode(final Chain chain, final Node node) {
		if (node == null) {
			return null;
		}
		Set<Edge> possibleEdges = new HashSet<Edge>();
		for (Edge edge : chain.getEdges()) {
			if (edge.getDestination() == node) {
				possibleEdges.add(edge);
			}
		}
		if (possibleEdges.size() == 1) {
			return possibleEdges.iterator().next().getSource();
		}
		return null;
	}

	/**
	 * Determines whether given {@link Node} and {@link Edge} can be appended to
	 * given {@link Chain} at the given toNode position, without causing it to
	 * loop or be incomplete.
	 * <EM>Note that no operation is to be performed on given Chain
	 * (it is caller's responsibility to actually perform the addition).
	 * </EM> <br/>
	 * <em>!!! Pending the {@link #isLooping(Chain)} implementation
	 * will always return false !!!</em>
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @param toNode
	 *            {@link Node}
	 * @param node
	 *            {@link Node}
	 * @param edge
	 *            {@link Edge}
	 * @return true only in case of possible append operation.
	 */
	public static boolean canAppendStep(final Chain chain, final Node toNode,
			final Node node, final Edge edge) {
		return false;
	}

	/**
	 * Determines whether given {@link Node} and {@link Edge} can be prepended
	 * to given {@link Chain} at the given toNode position, without causing it
	 * to loop or be incomplete.
	 * <EM>Note that no operation is to be performed on given Chain
	 * (it is caller's responsibility to actually perform the addition).
	 * </EM> <br/>
	 * <em>!!! Pending the {@link #isLooping(Chain)} implementation
	 * will always return false !!!</em>
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @param toNode
	 *            {@link Node}
	 * @param node
	 *            {@link Node}
	 * @param edge
	 *            {@link Edge}
	 * @return true only in case of possible prepend operation.
	 */
	public static boolean canPrependStep(final Chain chain, final Node toNode,
			final Node node, final Edge edge) {
		return false;
	}

	/**
	 * Creates a copy of given {@link Chain} (same {@link Nodes} and
	 * {@link Edges} series). <BR/>
	 * <EM>It is caller's responsibility to perform any required operation
	 * for this copy to be includes in a Resource for serialization purposes:
	 * this method only creates the Chain object and does not attach it to
	 * any EMF container.</EM>
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @return a copy of given chain.
	 */
	public static Chain copy(final Chain chain) {
		EcoreUtil.Copier copier = new EcoreUtil.Copier();
		Chain copy = (Chain) copier.copy(chain);
		copier.copyReferences();
		return copy;
	}

	/**
	 * Returns the {@link Node} this chain has at the end of it's Nodes list. It
	 * is null in case the chain's nodes list is empty.
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @return the last Node listed in this chain's Nodes (may be null).
	 */
	public static Node getLastListedNode(final Chain chain) {
		EList<Node> nodes = chain.getNodes();
		if (nodes == null || nodes.isEmpty()) {
			return null;
		}
		int nodeSize = nodes.size();
		Node finalNode = chain.getNodes().get(nodeSize - 1);
		return finalNode;
	}

	/**
	 * Returns the Node this chain has at the beginning of it's Nodes list. It
	 * is null in case the chain's nodes list is empty.
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @return the first Node listed in this chain's Nodes (null when no nodes
	 *         are listed).
	 */
	public static Node getFirstListedNode(final Chain chain) {
		if (chain.getNodes() == null || chain.getNodes().isEmpty()) {
			return null;
		}
		return chain.getNodes().get(0);
	}

	/**
	 * Returns the Edge in the given Chain having the given from Node as source
	 * and to Node as destination. Null is to be returned in case :
	 * <UL>
	 * <LI>One of chain or nodes are null.</LI>
	 * <LI>no solution or multiple solutions are found.</LI>
	 * </UL>
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @param from
	 *            {@link Node}
	 * @param to
	 *            {@link Node}
	 * @return the edge in the chain connecting the two given nodes (may be
	 *         null).
	 */
	public static Edge getEdge(final Chain chain
			, final Node from, final Node to) {
		Set<Edge> edges = new HashSet<Edge>();
		for (Edge edge : chain.getEdges()) {
			if (edge.getSource() == from && edge.getDestination() == to) {
				edges.add(edge);
			}
		}
		if (edges.size() == 1) {
			return edges.iterator().next();
		}
		return null;
	}

	/**
	 * Makes the substitution of the series of nodes in given chain between
	 * startNode and EndNode with a DecompositionNode. The removed nodes, edge
	 * series are added to the DecompositionNode representationChains. The Chain
	 * is modified by this operation. The success flag returned indicates
	 * whether the substitution has been successful.
	 * 
	 * @param chain
	 *            {@link Chain}
	 * @param startNode
	 *            {@link Node}
	 * @param endNode
	 *            {@link Node}
	 * @param decompNode
	 *            {@link DecomposableNode}
	 * @param start2Decomp
	 *            {@link Edge}
	 * @param decomp2End
	 *            {@link Edge}
	 * @return true in case of success.
	 */
	public static boolean composeChain(final Chain chain, final Node startNode,
			final Node endNode, final DecomposableNode decompNode,
			final Edge start2Decomp, final Edge decomp2End) {

		Chain subChain = CoredefFactory.eINSTANCE.createChain();
		ProxyNode pStartNode = NodeHelper.createProxyNode(startNode);
		subChain.setStartNode(pStartNode);

		ProxyNode pEndNode = NodeHelper.createProxyNode(endNode);
		subChain.setEndNode(pEndNode);

		EList<Node> allNodes = new BasicEList<Node>();
		allNodes.add(chain.getStartNode());
		allNodes.addAll(chain.getNodes());
		allNodes.add(chain.getEndNode());
		int startInt = allNodes.indexOf(startNode);
		int endInt = allNodes.indexOf(endNode);

		if (startInt == -1 || endInt == -1 || endInt <= startInt) {
			// Some pb here let's do nothing and return false
			return false;
		}

		Node prevNode = startNode;
		Node prevPNode = pStartNode;
		Node curNode = null;
		Edge curEdge = null;

		for (int i = startInt + 1; i < endInt; i++) {
			curNode = allNodes.get(i);
			curEdge = ChainHelper.getEdge(chain, prevNode, curNode);
			ProxyNode pNode = NodeHelper.createProxyNode(curNode);
			subChain.getNodes().add(pNode);
			ProxyEdge pEdge = EdgeHelper.createProxyEdge(curEdge);
			pEdge.setSource(prevPNode);
			pEdge.setDestination(pNode);
			pEdge.setRepresentedObject(curEdge);
			subChain.getEdges().add(pEdge);
			prevNode = curNode;
			prevPNode = pNode;
		}
		EList<Edge> extraEdges = subChain.getEdges();
		Edge edge = ChainHelper.getEdge(chain, curNode, endNode);
		if (edge != null) {
			ProxyEdge pEdge = EdgeHelper.createProxyEdge(edge);
			pEdge.setSource(prevPNode);
			pEdge.setDestination(pEndNode);
			pEdge.setRepresentedObject(edge);
			subChain.getEdges().add(pEdge);
			// subChain.getEdges().add(edge);
		}
		Logger.log(ChainHelper.stringRepresentation(subChain));
		// creating substitution Node
		boolean add = true;
		for (Chain repChain : decompNode.getRepresentedChains()) {
			if (ChainHelper.areComparable(repChain, subChain)) {
				add = false;
				break;
			}
		}
		if (add) {
			decompNode.getRepresentedChains().add(subChain);
		}
		// Make substitution in our chain...
		chain.getNodes().removeAll(subChain.getNodes());
		for (Node node : subChain.getNodes()) {
			if (node instanceof ProxyNode) {
				chain.getNodes().remove(
						((ProxyNode) node).getRepresentedObject());
			}
		}
		chain.getEdges().removeAll(extraEdges);
		for (Edge extraEdge : extraEdges) {
			if (extraEdge instanceof ProxyEdge) {
				chain.getEdges().remove(
						((ProxyEdge) extraEdge).getRepresentedObject());
			}
		}
		chain.getEdges().add(start2Decomp);
		int newStart = chain.getNodes().indexOf(startNode);
		chain.getNodes().add(newStart + 1, decompNode);
		chain.getEdges().add(decomp2End);
		Logger.log(ChainHelper.stringRepresentation(chain));
		return true;
	}

	/**
	 * Produces a string representation of the {@link Chain}. in the form :<BR/>
	 * <TT>
	 * Start->End : Start + One + ... + End
	 * </TT>
	 * 
	 * @param chain
	 *            the {@link Chain}.
	 * @return string representation of the chain.
	 */
	public static String stringRepresentation(final Chain chain) {
		Node startNode = chain.getStartNode();
		Node endNode = chain.getEndNode();
		Node firstNode = ChainHelper.getFirstListedNode(chain);
		EList<Node> nodes = chain.getNodes();
		EList<Edge> edges = chain.getEdges();
		StringBuffer buffer = new StringBuffer();

		String prettyStart = "";
		if (startNode instanceof DescribedObject) {
			prettyStart = ((DescribedObject) startNode).getName();
		} else {
			prettyStart = startNode.toString();
		}
		String prettyEnd = "";
		if (endNode instanceof DescribedObject) {
			prettyEnd = ((DescribedObject) endNode).getName();
		} else {
			prettyEnd = endNode.toString();
		}

		String sep = " , ";
		buffer.append(prettyStart);
		buffer.append("->");
		buffer.append(prettyEnd);
		buffer.append(" : ");
		if (firstNode == null) {
			buffer.append("No nodes listed !!");
		} else {
			buffer.append(prettyStart);
			for (Edge edge : edges) {
				if (edge.getSource() == startNode
						&& edge.getDestination() == firstNode) {
					sep = " + ";
					break;
				}
			}
			buffer.append(sep);
			Node prevNode = startNode;
			sep = " , ";
			for (Node node : nodes) {
				if (node instanceof DescribedObject) {
					buffer.append(((DescribedObject) node).getName());
				} else {
					buffer.append(node);
				}
				for (Edge edge : edges) {
					if (edge.getSource() == prevNode
							&& edge.getDestination() == node) {
						sep = " + ";
						break;
					}
				}
				buffer.append(sep);
				sep = " , ";
				prevNode = node;
			}
			// delete last separator
			buffer.delete(buffer.length() - sep.length(), buffer.length());

			for (Edge edge : edges) {
				if (edge.getSource() == prevNode
						&& edge.getDestination() == endNode) {
					sep = " + ";
					break;
				}
			}

			buffer.append(sep);
			buffer.append(prettyEnd);
		}
		return buffer.toString();
	}

	/**
	 * Determines whether the two given {@link Chain}s are comparable. To be
	 * comparable they must have :<br/>
	 * <ul>
	 * <li>the same (or comparable) ordered series of {@link Node}s</li>
	 * <li>and the same (or comparable) ordered series of {@link Edges}s</li>
	 * </ul>
	 * 
	 * @param chain1
	 *            {@link Chain}
	 * @param chain2
	 *            {@link Chain}
	 * @see NodeHelper#areComparable(EList, EList)
	 * @see EdgeHelper#areComparable(Edge, Edge)
	 * @return true only if the {@link Chain}s are comparable.
	 */
	public static boolean areComparable(final Chain chain1
			, final Chain chain2) {

		if (chain1 == chain2) {
			return true;
		}
		if (chain1 != null) {
			EList<Node> allNodes1 = new BasicEList<Node>();
			allNodes1.add(chain1.getStartNode());
			allNodes1.addAll(chain1.getNodes());
			allNodes1.add(chain1.getEndNode());
			EList<Node> allNodes2 = new BasicEList<Node>();
			allNodes2.add(chain2.getStartNode());
			allNodes2.addAll(chain2.getNodes());
			allNodes2.add(chain2.getEndNode());
			if (allNodes1.size() != allNodes2.size()) {
				return false;
			}
			for (int i = 0; i < allNodes1.size(); i++) {
				if (!NodeHelper.areComparable(allNodes1.get(i), allNodes2
						.get(i))) {
					return false;
				}
			}

			if (chain1.getEdges().size() != chain2.getEdges().size()) {
				return false;
			}
			for (Edge edge1 : chain1.getEdges()) {
				boolean found = false;
				for (Edge edge2 : chain2.getEdges()) {
					if (EdgeHelper.areComparable(edge1, edge2)) {
						found = true;
						break;
					}
				}
				if (!found) {
					return false;
				}
			}
			return true;
		}
		return false;
	}
}
